﻿namespace Lilium.CSharp

module Expressions = 
    
    open System.Linq.Expressions
    
    open Microsoft.FSharp.Quotations

    // Patterns

    let (|Call|_|) (expression: Expression) = 
        match expression.NodeType with
        | ExpressionType.Call ->
            let call = expression :?> MethodCallExpression 
            Some (call.Method, call.Object, call.Arguments |> Seq.toList)
        | _ -> None

    let (|Constant|_|) (expression: Expression) =
        match expression.NodeType with 
        | ExpressionType.Constant ->
            let constant = expression :?> ConstantExpression
            Some (constant.Value, constant.Type)
        | _ -> None

    let (|Lambda|_|) (expression: Expression) =
        match expression.NodeType with
        | ExpressionType.Lambda ->
            let lambda = expression :?> LambdaExpression
            Some (lambda.Body, lambda.Parameters |> Seq.toList)
        | _ -> None

    let (|Parameter|_|) (expression: Expression) =
        match expression.NodeType with
        | ExpressionType.Parameter ->
            let parameter = expression :?> ParameterExpression
            Some (parameter.Name, parameter.Type)
        | _ -> None

    // Expression translator

    let private findVariabe scope name = 
        match List.tryPick (Map.tryFind name) scope with
        | Some variable -> variable
        | None -> failwith (sprintf "Variable %s not in scope" name)
        
    let rec private toExprImpl scope expression = 
        match expression with 
        | Call (method', object', arguments) -> Expr.Call (toExprImpl scope object', method', List.map (toExprImpl scope) arguments)
        | Constant (value, type') ->  Expr.Value (value, type')
        | Lambda (body, parameters) -> toLambda scope body parameters
        | Parameter (name, _) -> Expr.Var (findVariabe scope name)
        | _ -> failwith (sprintf "Unable to convert %O to F# expression" expression.NodeType)

    and private toLambda scope body (parameters : ParameterExpression List) =
        match parameters with
        | [] -> Expr.Lambda (Var("unitArg", typeof<unit>), toExprImpl scope body)
        | _  -> 
            let variables = parameters |> List.map (fun p -> (p.Name, Var(p.Name, p.Type)))
            let body = toExprImpl (Map.ofList variables :: scope) body
            List.foldBack (fun (_, var) expr -> Expr.Lambda(var, expr)) variables body

    let toExpr expression = toExprImpl [] expression